// Copyright © 2010-2014 Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE file included with this file.
"use strict";

{
    let {
        ADDON_ID,
        PREFIX,
        Cc,
        Ci,
        Cu,
        Services,
        XUL,
        _,
        allPrefs,
        categories,
        compartments,
        debug,
        getSlowSQLTime,
        iter,
        iterCategory,
        jsonToDOM,
        locations,
        log,
        minimizeMemory,
        module,
        newValuesMsg,
        prefs,
        refreshCompartments,
        trackNodes,
        quitApplication,
        wrap,
        xpath
    } = Components.utils.import("resource://exttest/exttest.jsm", {});

    // Install any restartless extensions requested by test harness.
    let urls = prefs.get("restartless-extension-install-urls", "");
    prefs.set("restartless-extension-install-urls", "");

    if (urls)
        setTimeout(() => {
            Components.utils.import("resource://gre/modules/AddonManager.jsm");
            JSON.parse(urls).forEach(url => {
                AddonManager.getInstallForURL(url,
                                              install => { install.install(); },
                                              "application/x-xpinstall");
            })
        }, 1000);


    let file = document.documentURI.replace(/.*\//, "");

    let { hasOwnProperty } = {};

    let $ = function $(selector, node) (node || document).querySelector(selector);
    let $$ = function $$(selector, node) (node || document).querySelectorAll(selector);

    let domWindowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIDOMWindowUtils);

    // From XPIProvider.jsm
    let gIDTest = /^(?:\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)(\.xpi)?$/i;

    // Clear legacy preferences
    ["IDs", "properties"].forEach(function (pref) { prefs.reset(pref); });

    if (prefs.safe.has("Array"))
        ["Array", "Boolean", "Date", "Function", "Number", "Object", "RegExp", "String"]
            .forEach(function (pref) {
                prefs.safe.set(pref + ".prototype", prefs.safe.get(pref));
                prefs.safe.reset(pref);
            });


    let prevValues = {};
    let labels = {};
    let generatedDOM = false;
    let generateDOM = function generateDOM() {
        if (generatedDOM)
            return;
        generatedDOM = true;
        let createContents = Services.has("dactyl") && Services.dactyl.createContents;

        let bar = document.getElementById("PersonalToolbar");
        if (bar && bar.collapsed) {
            setToolbarVisibility(bar, true);
            setToolbarVisibility(bar, false);
        }

        let menu = document.getElementById("main-menubar");
        if (!menu)
            return;

        frob(menu);
        function frob(node) {
            if (createContents)
                createContents(node);
            else
                node.open = true;

            Array.filter(node.childNodes, function (n) ~["menu", "menupopup"].indexOf(n.localName))
                 .forEach(frob);

            if (!createContents)
                node.open = false;
        }
    };

    let total;
    let compare = function compare(name, shortName, values, global, params) {
        let { listMunger, menuItems, munger, separator } = params || {};

        let _seen = {};
        values = values.filter(function (val) !hasOwnProperty.call(_seen, val) && (_seen[val] = true));

        let safeValues = values.sort();
        if (prefs.safe.has(name))
            safeValues = JSON.parse(prefs.safe.get(name));
        else
            prefs.safe.set(name, JSON.stringify(values));

        values = values.filter(function (p) !~safeValues.indexOf(p)).sort();
        let newValues = values;
        if (name in prevValues)
            newValues = values.filter(function (v) !~this.indexOf(v), prevValues[name]);
        prevValues[name] = values;

        let count = values.length;
        if (prefs.status.get(name, true))
            total += count;

        if (munger) {
            values = values.map(munger);
            newValues = newValues.map(munger);
        }
        if (listMunger) {
            values = listMunger(values);
            newValues = listMunger(newValues);
        }

        Array.forEach(buttons, function (button) {
            if (values.length && prefs.status.get(name, true)) {
                if (!(name in button.labels)) {
                    button.appendChild(jsonToDOM(["toolbarbutton", { class: "exttest-label toolbarbutton-1",
                                                                     context: "exttest-context-count",
                                                                     oncommand: "this.copy()",
                                                                     key: name,
                                                                     xmlns: XUL },
                        ["label", { class: "exttest-label-title", value: (shortName || name[0]) + ":" }],
                        ["label", { class: "exttest-label-count", key: name + "-count" }]],
                        document, button.labels));

                    button.labels[name].reset = function reset() {
                        if (shortName == "ID")
                            generateDOM();
                        prefs.safe.reset(this.getAttribute("key"));
                        this.hidden = true;
                    };
                    button.labels[name].onclick = function (event) {
                        if (event.button == 1 && menuItems)
                            for (let [k, v] in Iterator(menuItems)) {
                                v.call(button.labels[name], event);
                                break;
                            }
                    };
                    button.labels[name].menuItems = menuItems;
                }

                button.labels[name + "-count"].value = count;
                button.labels[name].hidden = false;
                button.labels[name].setAttribute("tooltiptext", wrap(values, newValuesMsg(name) + " ", separator || " "));
                button.labels[name].setAttribute("value", values.join(separator || " "));
                button.labels[name].copy = function copy() {
                    let data = this.getAttribute("value");
                    Services.clipboardHelper.copyString(data);
                };
            }
            else if (name in button.labels) {
                button.labels[name].hidden = true;
            }
        });

        if (!global && newValues.length && prefs.console.get(name, true))
            log(newValuesMsg(name, "added-to"), newValues);
    };


    let ignoreGlobals = {
        __SS_dyingCache: true,
        __SS_lastSessionWindowID: true,
        __SS_restoreID: true,
        __SS_tabsToRestore: true,
        __SSi: true,
        exttest_jQuery_locations: true
    };

    let checkUndefined = {
        jQuery: true,
        $: true
    };

    let monetizationHosts = {
        "www.superfish.com": true,
        "cdn.foxydeal.com": true
    };
    let monetizationPatterns = [
        RegExp('^https?://s3.amazonaws.com/cdn.foxydeal.com/')
    ];

    let monetizationScripts = {};

    let checkMonetization = function checkMonetization(script) {
        if (!script.src)
            return;

        let uri = Services.io.newURI(script.src, null, null);
        if (~["http", "https"].indexOf(uri.scheme)
                && (hasOwnProperty.call(monetizationHosts, uri.host) ||
                    monetizationPatterns.some(function (p) p.test(uri.spec)))) {
            monetizationScripts[script.src] = true;
            log(_("monetizationScript", script.src, script.ownerDocument.documentURI));
        }
    }


    let getButtons = function getButtons() $$("#exttest-toolbar-counters, #exttest-status");
    let buttons;
    let sandbox = Cu.Sandbox(window);
    let check = function check() {
        buttons = getButtons();
        Array.forEach(buttons, function (button) {
            if (!button.labels)
                button.labels = {};
        });

        if (!prefs.safe.has("global." + file)) {
            // Touch as many known properties as possible, since they
            // tend not to show up until we do, at which point they appear
            // as loose properties

            const DEPRECATED = ["nsIDOMWindowInternal", "nsIPrefBranch2"]

            for each (let iface in Ci)
                if (iface instanceof Ci.nsIJSIID && !DEPRECATED.indexOf(iface))
                    window instanceof iface;

            for (let k in Ci)
                if (/^nsIDOM/.test(k))
                    if (k.substr(6) in window && !~DEPRECATED.indexOf(k))
                        window[k.substr(6)];
            for (let k in Ci)
                if (k.substr(3) in window)
                    window[k.substr(3)];

            for (let cat of categories)
                for (let property in iterCategory(cat))
                    if (property in window)
                        try { window[property]; } catch (e) { Cu.reportError(e); }

            ["Application", "Array", "ArrayBuffer", "AttributeName", "Boolean",
             "Components", "CSSFontFaceStyleDecl", "CSSGroupRuleRuleList",
             "CSSNameSpaceRule", "CSSRGBColor", "CSSRect", "ComputedCSSStyleDeclaration",
             "Date", "Error", "EvalError", "Float32Array", "Float64Array", "Function",
             "HTMLDelElement", "HTMLInsElement", "HTMLSpanElement", "Infinity",
             "InnerModalContentWindow", "InnerWindow", "Int16Array", "Int32Array",
             "Int8Array", "InternalError", "Iterator", "JSON", "KeyboardEvent", "Math",
             "NaN", "Namespace", "Number", "Object", "Proxy", "QName",
             "ROCSSPrimitiveValue", "RangeError", "ReferenceError", "RegExp",
             "StopIteration", "String", "SyntaxError", "TypeError", "URIError",
             "Uint16Array", "Uint32Array", "Uint8Array", "XML", "XMLHttpProgressEvent",
             "XMLList", "XMLSerializer", "XMLStylesheetProcessingInstruction",
             "XPCNativeWrapper", "XPCSafeJSWrapper", "XULControllers", "_content",
             "constructor", "decodeURI", "decodeURIComponent", "encodeURI",
             "encodeURIComponent", "escape", "eval", "isFinite", "isNaN", "isXMLName",
             "parseFloat", "parseInt", "undefined", "unescape", "uneval"
            ].concat(Object.getOwnPropertyNames({}.__proto__))
             .forEach(function (k) { if (k in window) window[k]; });
        }


        total = 0;

        // Global properties
        compare("global." + file, "g", [].concat.apply([], [window, window.__proto__, sandbox]
                                                             .map(o => {
                                                                 try {
                                                                     return Object.getOwnPropertyNames(o);
                                                                 }
                                                                 catch (e) {
                                                                     Cu.reportError(e);
                                                                     return [];
                                                                 }
                                                             }))
                                         .filter(function (p) !(/\s/.test(p) || ignoreGlobals.hasOwnProperty(p)) &&
                                                              !(/^\d+$/.test(p) && window[p] instanceof Window) &&
                                                              !(checkUndefined.hasOwnProperty(p) && window[p] === undefined)));


        // DOM node IDs
        compare("IDs." + file, "ID", [a.value for (a in xpath("//@id", document))
                                      if (!/^(spring\d+$|exttest-)/.test(a.value))]);


        // Categories
        let cats = Array.concat.apply(null,
            categories.map(function (cat) [cat.replace(/\s/g, "-") + ":" + prop
                                           for (prop in iterCategory(cat))]));
        compare("categories", "cat", cats);


        compare("Math", "M", Object.getOwnPropertyNames(Math));


        // Object prototypes
        ["Array",
         "Boolean",
         "Date",
         "Function",
         "Number",
         "Object",
         "RegExp",
         "String",
         "XMLHttpRequest"].forEach(function (name) {
            compare(name + ".prototype", name[0] + ".p", Object.getOwnPropertyNames(window[name].prototype));
        });


        // XPCOM classes and interfaces
        [["Cc", Cc],
         ["Ci", Ci]].forEach(function ([name, obj]) {
            compare(name, name, Object.getOwnPropertyNames(obj));
        });


        compare("Preference", "p",
                allPrefs.getNames().map(function (p) p.replace(/^((?:extensions\.|capability\.policy\.)?[^.]+(?:\.(?:com|net|org))?(?:\.|$)).*/, "$1")));


        ["setInterval", "setTimeout"].forEach(function (name) {
            if ("exttestLocations" in window[name]) {
                compare(name, name[0]+name[3],
                        Object.getOwnPropertyNames(window[name].exttestLocations),
                        true,
                        { separator: "\n" + PREFIX });
            }
        });


        compare("jQuery", "jQ",
                Object.getOwnPropertyNames(window.exttest_jQuery_locations || {}),
                true,
                { separator: "\n" + PREFIX });


        compare("XMLHttpRequest", "XHR",
                Object.getOwnPropertyNames(locations.xmlhttp),
                true,
                { separator: "\n" + PREFIX });


        compare("Monetization", "M",
                Object.getOwnPropertyNames(monetizationScripts),
                true,
                { separator: "\n" + PREFIX });


        compare("<script>", "S",
                Object.getOwnPropertyNames(locations.scripts),
                true,
                { separator: "\n" + PREFIX });


        compare("<iframe>/<browser>", "F",
                Object.getOwnPropertyNames(locations.iframes),
                true,
                { separator: "\n" + PREFIX });


        compare("Insecure", "I",
                Object.getOwnPropertyNames(locations.insecure),
                true,
                { separator: "\n" + PREFIX });


        compare("Tracking", "T",
                Object.getOwnPropertyNames(locations.tracking),
                true,
                { separator: "\n" + PREFIX });


        if (false) {
            // Not ready yet.
            let roots = [];
            for (let file in iter(extensionsDir.directoryEntries, Ci.nsILocalFile))
                if (gIDTest.test(file.leafName) && file.isDirectory())
                    roots.push(file.lastModifiedTime+":"+file.getRelativeDescriptor(extensionsDir));

            compare("Modified extension roots", "E",
                    roots,
                    false,
                    function (n) n.replace(/^\d+:/, ""));
        }

        let counter = $("#exttest-toolbar-count");
        if (counter) {
            counter.textContent = total;
            counter.style.visibility = total ? "" : "collapse";
        }

        if (compartments)
            compare("Compartments", "C",
                    compartments,
                    true, {
                        menuItems: { minimize: doMinimize },
                        listMunger: function (items) {
                            let obj = {};
                            items.forEach(function (i) { obj[i] = (obj[i] || 0) + 1 });
                            return [item + (count > 1 ? " (" + count + ")" : "")
                                    for ([item, count] in Iterator(obj))];
                        },
                        munger: function (n) n.replace(/ \(\d+\)$/, "")
                                              .replace(/, 0x[0-9a-f]+\b/, "")
                                              .replace(/((?:chrome|resource):\/\/(?:[^\/]*)\/)\S*/i, "$1...")
                                              .replace(/(jar:file:\/\/.*!)\S*/i, "$1..."),
                        separator: "\n" + PREFIX
                    });

        if (!locations.foundBrokenIncGC
                &&  domWindowUtils.isIncrementalGCEnabled
                && !domWindowUtils.isIncrementalGCEnabled()
                && prefs.get("javascript.options.mem.gc_incremental")) {
            locations.foundBrokenIncGC = true;
            log(_("incgc.broken"), null, {}, true);
        }

        let clock = $("#exttest-toolbar-clock");
        if (clock) {
            let label = $("#exttest-toolbar-slow-sql-container");

            let t = getSlowSQLTime();

            if (t <= 6000)
                clock.setAttribute("time",
                                   Math.min(360,
                                            Math.round(12 * t / 6000) * 30));
            else
                clock.setAttribute("time", "r" +
                                   Math.min(360,
                                            Math.round(12 * t / 60000) * 30));

            clock.collapsed = t == 0;
            label.collapsed = t == 0;

            $("#exttest-toolbar-slow-sql").value = t.toLocaleString() + "ms";
        }
    };

    let doMinimize = function doMinimize() {
        minimizeMemory(function () {
            refreshCompartments();
            timeout();
        });
    };

    let extensionsDir = Services.directory.get("ProfD", Ci.nsILocalFile);
    extensionsDir.append("extensions");

    ["setInterval", "setTimeout", "XMLHttpRequest"].forEach(function (name) {
        if (!prefs.safe.has(name))
            prefs.safe.set(name, "[]");
    });


    addEventListener("load", function onLoad() {
        removeEventListener("load", onLoad, false);

        if (navigator.platform == "Win32")
            document.getElementById("exttest-key-quitApplication").removeAttribute("disabled");

        function update(version) {
            if (Services.versionCompare.compare(version, "2.13") < 0 && !$("#exttest-toolbar-button")) {
                let addonBar = $("#addon-bar");
                let navBar = $("#nav-bar");
                if (addonBar && addonBar.parentNode.id != "navigator-toolbox") {
                    addonBar.insertItem("exttest-toolbar-button");
                    document.persist(addonBar.id, "currentset");
                    addonBar.collapsed = false;
                }
                else if (navBar) {
                    navBar.insertItem("exttest-toolbar-button");
                    document.persist(navBar.id, "currentset");
                }
            }
        }

        function checkVersion(addon) {
            let version = prefs.get("version", null);
            if (version != addon.version)
                update(version || "0", addon.version);

            prefs.set("version", addon.version);
        }


        if ("nsIExtensionManager" in Ci)
            checkVersion(Services.extensionManager.getItemForID(ADDON_ID))
        else {
            // Import it into the global scope to prevent loose variable
            // warnings when other extensions import it.
            Components.utils.import("resource://gre/modules/AddonManager.jsm");
            AddonManager.getAddonByID(ADDON_ID, checkVersion);
        }


        Array.forEach($("#exttest-context-button").children, function (child) {
            if (child.hasAttribute("test"))
                child.hidden = !(child.getAttribute("test") in window);
        });

        $("#exttest-context-restart").addEventListener("command", function (event) {
            quitApplication(true);
        }, false);

        $("#exttest-context-minimize-memory").addEventListener("command", doMinimize, false);

        $("#exttest-key-menu").addEventListener("command", function (event) {
            for each (let button in Array.slice(getButtons())) {
                let rect = button.getBoundingClientRect();
                if (button && rect.height) {
                    $("#exttest-context-button").openPopup(button, "after_start", null, null, true, null, event);
                    break;
                }
            }
        }, false);

        $("#exttest-context-count").addEventListener("popupshowing", function (event) {
            let button = document.popupNode;
            Array.forEach($$(".temporary", this), function (elem) elem.parentNode.removeChild(elem));
            if (button.menuItems)
                for (let [k, v] in Iterator(button.menuItems))
                    this.appendChild(jsonToDOM(
                                ["menuitem", { class: "temporary",
                                               label: _("test-context." + k + ".label"),
                                               accesskey: _("test-context." + k + ".accesskey"),
                                               tooltiptext: _("test-context." + k + ".tooltip"),
                                               xmlns: XUL }],
                                document))
                        .addEventListener("command", v);
        }, false);

        if (typeof gBrowser != "undefined")
            gBrowser.addEventListener("DOMContentLoaded", function (event) {
                trackNodes(event.target, "script[src]", checkMonetization);
            }, true);

        let copy = $("#exttest-context-copy");
        if ($("#context-copy"))
            copy.style.listStyleImage = window.getComputedStyle($("#context-copy"), null).listStyleImage;


        timeoutId = setTimeout(timeout, 1000);

    }, false);

    let timeoutId;
    let timeout = function timeout() {
        try { clearTimeout(timeoutId) } catch (e) {}
        try {
            check();
        }
        catch (e) {
            Cu.reportError(e);
        }
        timeoutId = setTimeout(timeout, prefs.get("interval"));
    }

    /**
        XMLHttpRequest().open("GET", "chrome://exttest/content/exttest.js", false);
        XMLHttpRequest().open("GET", "http://www.google.com", false);
        XMLHttpRequest().open("GET", "http://www.google.com", true, "foo", "bar");
        XMLHttpRequest().open("GET", "https://www.google.com", true, "foo", "bar");
        XMLHttpRequest().open("GET", "http://www.google.com", true);
        XMLHttpRequest().open("GET", "http://www.google.com/?pass=foo", true);
        XMLHttpRequest().open("GET", "http://www.google.com/?user=foo&pass=bar", true);
        clearInterval(setInterval("12", 12));
        setTimeout("12", 12);
    /**/

    /**
    let reportError = function reportError(e) {
        Cu.reportError(e); dump("Error: " + e + "\n" + (e.stack || Error().stack));
    }

    gBrowser.addEventListener("click", function load(event) {
        gBrowser.removeEventListener("click", load, true);

        const JQUERY = "file:///home/kris/extension-test/jquery/jquery-1.6.2.js";
        const SCRIPT = "data:application/javascript,dump(12)";
        const JSON   = "file:///home/kris/foo/bar", P = "callback=?";
        const JSONP  = "file:///home/kris/foo/bar?callback=?";

        window.Services.scriptloader.loadSubScript(JQUERY, window);
        let elem = content.document.body;
        try {
            jQuery(elem).html('<div>html</div>');
            jQuery(elem).append('<div>append</div>');
            jQuery(elem).prepend('<div>prepend</div>');
            jQuery("div", elem).before('<div>before</div>');
            jQuery("div", elem).after('<div>after</div>');
            jQuery('<div/>');
            jQuery('<script/>');
            jQuery.globalEval('dump(12)');

            jQuery.getScript(SCRIPT);
            jQuery.ajax({ url: SCRIPT, dataType: "script" });
            jQuery.get(SCRIPT, function () {}, "script");
            jQuery.get(SCRIPT, "", function () {}, "script");

            jQuery.ajax({ url: JSONP });
            jQuery.get(JSONP, function () {});

            jQuery.ajax({ url: JSON, data: P });
            jQuery.get(JSON, P, function () {});
        }
        catch (e) { reportError(e) }
        // jQuery("#home-button").load("file:///home/kris/form.xhtml");
    }, true);
    /**/
}
